Impressão
A impressão é acessível por meio de client.printer, disponível após a conexão com o serviço BTG Pay descrita na Configuração. As operações de impressão são assíncronas (suspend) e retornam Result<Unit> — nenhuma exceção escapa do método.
O conceito de job
A impressão funciona por meio de jobs. Dentro do bloco print { }, os elementos são declarados na ordem em que serão impressos no papel. Cada elemento — texto, imagem, QR code ou avanço de papel — é adicionado sequencialmente, e o SDK os envia à impressora nessa mesma ordem.
import btgpay.client.printer.Align
lifecycleScope.launch {
client.printer.print {
text("OLA MUNDO", size = 28, align = Align.CENTER, bold = true)
feed(lines = 4)
}.onSuccess {
Log.i(TAG, "impresso")
}.onFailure { e ->
Log.e(TAG, "falhou: ${e.message}")
}
}
Imprimir texto
O método text adiciona uma linha de texto ao job. O único parâmetro obrigatório é o conteúdo; os demais possuem valores padrão.
lifecycleScope.launch {
client.printer.print {
text("MERCADO SILVA", size = 28, align = Align.CENTER, bold = true)
text("CNPJ 30.306.294/0001-45", size = 14, align = Align.CENTER)
text("")
text("Rua das Flores, 123")
text("Sao Paulo - SP")
text("Obrigado pela preferencia!", align = Align.CENTER)
feed(lines = 4)
}
}
| Parâmetro | Tipo | Default | Descrição |
|---|---|---|---|
text | String | obrigatório | Conteúdo do texto. Máximo 4.096 caracteres. |
size | Int | 16 | Corpo da fonte. |
align | Align | Align.LEFT | LEFT, CENTER ou RIGHT. |
bold | Boolean | false | Negrito. |
marginLeft | Int | 0 | Margem esquerda, em pixels. |
marginRight | Int | 0 | Margem direita, em pixels. |
lineSpace | Int | 0 | Espaço extra entre linhas. |
A fonte utilizada é a do firmware da impressora — não há como escolher a família tipográfica por esse método. A quebra de linha também é controlada pelo firmware: uma linha maior que a largura do papel é quebrada automaticamente. O negrito é feito pela própria impressora, engrossando o traço, e não por uma segunda fonte.
Para controle total sobre tipografia, as alternativas são a impressão por imagem ou o Template de Comprovante.
Imprimir imagem
O método image aceita exclusivamente PNG. O papel tem 384 pixels de largura, e o SDK não reescala a imagem — ela deve ser gerada já na largura correta.
lifecycleScope.launch {
val logo: ByteArray = assets.open("logo.png").use { it.readBytes() }
client.printer.print {
image(logo, align = Align.CENTER)
text("MERCADO SILVA", size = 22, align = Align.CENTER, bold = true)
feed(lines = 4)
}
}
| Parâmetro | Tipo | Default | Descrição |
|---|---|---|---|
png | ByteArray | obrigatório | Bytes de um PNG. Máximo 512 KB. |
align | Align | Align.CENTER | LEFT, CENTER ou RIGHT. |
marginLeft | Int | 0 | Margem esquerda, em pixels. |
marginRight | Int | 0 | Margem direita, em pixels. |
Para gerar o PNG a partir de um Bitmap Android, o seguinte helper reescala a imagem para a largura do papel:
import android.graphics.Bitmap
import java.io.ByteArrayOutputStream
fun Bitmap.toPrinterPng(): ByteArray {
val alvo = 384
val escalado = if (width != alvo) {
Bitmap.createScaledBitmap(this, alvo, height * alvo / width, true)
} else {
this
}
return ByteArrayOutputStream().use { out ->
escalado.compress(Bitmap.CompressFormat.PNG, 100, out)
out.toByteArray()
}
}
Logo e arte vetorial ficam bem abaixo do limite de 512 KB. Conteúdo fotográfico, no entanto, pode ultrapassá-lo: um PNG de 384x1200 com conteúdo fotográfico passa de 1,3 MB. A recomendação é converter para escala de cinza ou reduzir a altura antes de enviar. Se o limite for estourado, o erro é explícito (image-too-large) e informa o índice do elemento.
A impressora é térmica e monocromática. A conversão de meio-tom é feita pelo driver da impressora, e preto sólido produz resultados mais previsíveis que gradiente.
Imprimir QR code
O QR code é rasterizado pelo SDK, não pelo firmware da impressora.
lifecycleScope.launch {
client.printer.print {
text("Consulte sua nota fiscal", align = Align.CENTER)
qr("https://nf.e/abc123")
feed(lines = 4)
}
}
| Parâmetro | Tipo | Default | Descrição |
|---|---|---|---|
content | String | obrigatório | Conteúdo codificado no QR. |
align | Align | Align.CENTER | LEFT, CENTER ou RIGHT. |
size | Int | 240 | Lado do QR, em pixels, no papel de 384 px. |
O parâmetro size merece atenção. Conteúdo mais longo exige mais módulos no QR, e cada módulo precisa de ao menos um pixel de lado. Um size que funciona para uma URL curta pode ser recusado para uma URL longa, porque os módulos ficariam menores que um pixel e o código sairia ilegível. O erro nesse caso é explícito (qr-render-failed) — o SDK nunca gera um código em branco. Para URLs de tamanho variável, usar size = 280 ou tratar a falha é uma abordagem segura.
Avançar o papel
O método feed avança o papel em linhas.
lifecycleScope.launch {
client.printer.print {
text("CUPOM")
feed(lines = 4)
}
}
Sem feed no final do job, a última linha impressa fica presa dentro do mecanismo da impressora e o operador não consegue destacar o cupom. Quatro linhas é um valor prático para esse espaço.
Reutilizar um job
A função printJob { } constrói um PrintJob imutável, que pode ser impresso quantas vezes forem necessárias. Essa abordagem é útil para emitir segunda via.
import btgpay.client.printer.printJob
import btgpay.client.printer.PrintJob
val comprovante = printJob {
text("MERCADO SILVA", size = 28, align = Align.CENTER, bold = true)
text("VIA DO CLIENTE", align = Align.CENTER)
qr("https://nf.e/abc123")
feed(lines = 4)
}
lifecycleScope.launch {
client.printer.print(comprovante) // via do cliente
client.printer.print(comprovante) // via do estabelecimento
}
PrintJob é um data class contendo a lista de elementos, o que permite inspecionar ou serializar o job antes de imprimi-lo.
Estado da impressora
O método status() consulta o estado atual da impressora e retorna um PrinterStatus.
import btgpay.client.printer.PrinterStatus
lifecycleScope.launch {
when (client.printer.status()) {
PrinterStatus.Ready -> imprimir()
PrinterStatus.NoPaper -> avisar("Coloque papel")
PrinterStatus.Overheated -> avisar("Impressora quente, aguarde")
PrinterStatus.Unavailable -> avisar("Impressora indisponível")
}
}
PrinterStatus é uma sealed interface, o que torna o when exaustivo — o compilador avisa se um estado novo for adicionado em versões futuras.
Consultar o estado antes de imprimir é opcional: o print já falha com o erro adequado se houver algum problema. A consulta é útil quando se deseja avisar o operador antes de iniciar um job longo.
Tratamento de erros
Nenhuma chamada de impressão lança exceção. O resultado vem em Result, e a falha é sempre uma PrintException.
import btgpay.client.printer.PrintException
lifecycleScope.launch {
client.printer.print { /* ... */ }.onFailure { e ->
val erro = e as PrintException
Log.e(TAG, "brn=${erro.brn} indice=${erro.failedElementIndex} msg=${erro.message}")
}
}
Os campos de PrintException são:
| Campo | Tipo | Descrição |
|---|---|---|
brn | String | Código estável do erro, ex. brn:btg:pay:hal:printer:out-of-paper. |
severity | String | Gravidade do erro. |
message | String | Mensagem legível. |
details | String? | Contexto adicional, quando houver. |
failedElementIndex | Int? | Índice do elemento que falhou, ou null. |
Atomicidade
Um job não é atômico. Se o papel acaba no terceiro elemento de cinco, os dois primeiros já estão impressos e não há como desfazê-los. O failedElementIndex indica exatamente onde o job parou, permitindo retomar a impressão a partir daquele ponto.
O exemplo abaixo demonstra uma estratégia de retomada. A cada tentativa, os elementos já impressos são descartados com base no failedElementIndex. Se o índice não estiver disponível (erro não vinculado a um elemento específico), a função encerra sem retentar.
suspend fun imprimirComRetomada(job: PrintJob) {
var elementosImpressos = 0
repeat(3) {
val restante = PrintJob(job.elements.drop(elementosImpressos))
val resultado = client.printer.print(restante)
if (resultado.isSuccess) return
val falha = resultado.exceptionOrNull() as? PrintException
val indice = falha?.failedElementIndex ?: return
elementosImpressos += indice
}
}
Códigos de erro
Hardware e estado da impressora:
| brn | Significado |
|---|---|
...:printer:out-of-paper | Sem papel. |
...:printer:overheating | Cabeça superaquecida. |
...:printer:hardware-failure | Falha de hardware. |
...:printer:printer-timeout | A impressora não respondeu. |
Job recusado antes de imprimir:
| brn | Significado |
|---|---|
...:printer:empty-job | Nenhum elemento no job. |
...:printer:too-many-elements | Acima de 32 elementos. |
...:printer:image-too-large | Imagem acima de 512 KB. |
...:printer:text-too-long | Texto acima de 4.096 caracteres. |
...:printer:payload-too-large | Job somando mais de 768 KB. |
...:printer:qr-render-failed | Conteúdo longo demais, ou size pequeno demais. |
...:printer:malformed-job | Job inválido na travessia AIDL. |
Concorrência e disponibilidade:
| brn | Significado |
|---|---|
...:printer:printer-busy | Outro job em andamento. |
...:printer:printer-unavailable | Sem impressora no terminal. |
...:printer:job-timed-out | O job passou do tempo máximo. |
...:printer:timeout | O serviço não respondeu. |
...:printer:transport-failure | Falha na chamada ao serviço, normalmente app desconectado. |
Timeouts
Cada chamada possui um limite de tempo, porque o transporte AIDL é assíncrono e não reporta nada se o processo do serviço morrer no meio da operação:
| Chamada | Limite |
|---|---|
print | 90 s |
setReceiptTemplate | 15 s |
status | 5 s |
Um timeout chega como PrintException com brn ...:printer:timeout. Ele não significa que o papel parou de sair — apenas que a resposta do serviço não chegou.
Cancelamento
Cancelar a coroutine não interrompe o papel que já está saindo da impressora. O cancelamento é propagado corretamente ao escopo pai (não é engolido como falha de impressão), mas o mecanismo físico da impressora não é interrompido.
Limites
| Limite | Valor |
|---|---|
| Largura do papel | 384 px |
| Elementos por job | 32 |
| Caracteres por texto | 4.096 |
| Bytes por imagem | 512 KB |
| Bytes por job (soma) | 768 KB |
A impressão é serial: um caminho de papel, um job por vez. Dois jobs concorrentes não se misturam — o segundo espera ou retorna com printer-busy.